/*
 * This file is Copyright 2002-2006 Loyola University Chicago,
 * Department of Computer Science and the Emerging Technologies Laboratory.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License via
 *    http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.citep.formats.ofx;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

abstract public class ParserBase {
	
	private static Pattern _DECLNAME_MATCH = Pattern.compile("[a-zA-Z][-_.a-zA-Z0-9]*\\s*");
	private static Pattern _DECLSTRINGLIT_MATCH = Pattern.compile("(\\'[^\\']*\\'|\"[^\"]*\")\\s*");
	private static Pattern _COMMENTCLOSE = Pattern.compile("--\\s*>");
	private static Pattern _MARKEDSECTIONCLOSE = Pattern.compile("]\\s*]\\s*>");

	private String _decl_otherchars = "";
	
	private int lineno;
	private int offset;
	protected String rawdata;
	
	abstract void error(String message);
	
	void reset() {
		lineno = 1;
		offset = 0;
	}

	int getCurrentLineNumber() {
		return lineno;
	}
	
	int getCurrentOffset() {
		return offset;
	}
	
	private int updatePos(int i, int j) {
		if(i  >= j)
			return j;
		
		String buf = rawdata;

		int pos = 0;
		int idx = -1;
		int nlines = 0;
		while((idx = buf.indexOf('\n',idx+1)) != -1) {
			pos = idx;
			nlines++;
		}
		
		if(nlines > 0) {
			lineno += nlines;
			offset = j - (pos+1);
		} else {
			offset += j - i;
		}
		
		return j;
	}
	
	protected int parseDeclaration(int i) throws Exception {
		String buf = rawdata;
		
		int j = i + 2;
		
		if(buf.length() < 2 || !buf.substring(i, j).equals("<!"))
			throw new Exception("Unexpected call to parseDeclaration");
		
		if(buf.length() == 2 || (buf.length() == 3 && rawdata.charAt(j) == '-'))
			return -1;
		
		int n = buf.length();
		String decltype;
		
		if(buf.substring(j,j+2).equals("--")) {
			return parseComment(i);
		} else if(buf.charAt(j) == '[') {
			return parseMarkedSection(i);
		} else {
			NamePos result = scanName(j,i);
			j = result.getPos();
			decltype = result.getName();
		}
		
		if(j < 0)
			return j;
			
		if(decltype.equals("doctype"))
			_decl_otherchars = "";

		while(j < n) {
			char c = buf.charAt(j);
			
			if(c == '>') {
				String data = buf.substring(i+2,j);
				if(decltype.equals("doctype"))
					handleDecl(data);
				else
					unknownDecl(data);
				return j+1;
			}
			if("\"'".indexOf(c) != -1) {
				Matcher matcher = _DECLSTRINGLIT_MATCH.matcher(buf);
				if(!matcher.find(j))
					return -1;
				j = matcher.end();
			} else if("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(c) != -1) {
				NamePos result = scanName(j,i);
				j = result.getPos();
			} else if(_decl_otherchars.indexOf(c) != -1) {
				j++;
			} else if(c == '[') {
				if(decltype == "doctype") {
					j = parseDocTypeSubset(j+1,i);
				} else if("attlist".equals(decltype) ||
						  "linktype".equals(decltype) ||
						  "link".equals(decltype) ||
						  "element".equals(decltype)) {
					error("unsupported '[' char in "+decltype+" declaration");
				} else {
					error("unexpected '[' char in declaration");
				}
			} else {
				error("unexpected " + buf.charAt(j) + " char in declaration");
			}
			
			if(j < 0)
				return j;
				
		}
		return -1; //incomplete
	}
	
	int parseMarkedSection(int i) throws Exception {
		return parseMarkedSection(i,true);
	}
	
	int parseMarkedSection(int i, boolean report) throws Exception {
		
		String buf = rawdata;
		
		if(rawdata.substring(i,i+3).equals("<!["))
			throw new Exception("unexpected call to parse_marked_section()");

		NamePos result = scanName(i+3,i);
		int j = result.getPos();
		String sectName = result.getName();
		
		if(j < 0)
			return j;
		
		Matcher matcher = null;
		
		if(	"temp".equals(sectName) ||
			"cdata".equals(sectName) ||
			"ignore".equals(sectName) ||
			"include".equals(sectName) ||
			"rcdata".equals(sectName)) {
			
			matcher = _MARKEDSECTIONCLOSE.matcher(buf);
		} else if(	"if".equals(sectName) ||
					"else".equals(sectName) ||
					"endif".equals(sectName)) {
			
			matcher = _MARKEDSECTIONCLOSE.matcher(buf);
		} else {
			error("unknown status keyword " + buf.substring(i+3,j) + " in marked section");
		}
		
		if(!matcher.find(i+3))
			return -1;
		
		if(report) {
			j = matcher.start(0);
			unknownDecl(buf.substring(i+3,j));
		}
		
		return matcher.end(0);
	}
	
	protected int parseComment(int i) {
		return parseComment(i, true);
	}
	
	private int parseComment(int i, boolean report) {
		String buf = rawdata;
		
		if((i+4) >= buf.length() || !buf.substring(i,i+4).equals("<!--")) {
			error("unexpected call to parseComment");
		}
		
		Matcher matcher = _COMMENTCLOSE.matcher(buf);
		if(!matcher.find(i+4))
			return -1;

		if(report) {
			int j = matcher.start();
			handleComment(buf.substring(i+4,j));
		}
		
		return matcher.end();
	}
	
	private int parseDocTypeSubset(int i, int declstartpos) {
		String buf = rawdata;
		
		int n = buf.length();
		int j = i;
		
		while(j < n) {
			char c = rawdata.charAt(j);
			if(c == '<') {
				
				String s = buf.substring(j,j+2);
				
				if(s.equals("<"))
					return -1;
				
				if(!s.equals("<!")) {
					updatePos(declstartpos, j+1);
					error("unexpected char in internal subset (in "+buf.substring(j)+")");
				}

				if((j+2) == n)
					return -1; //end of buffer, incomplete
				
				if((j+4) > n)
					return -1; //end of buffer, incomplete

				if(!buf.substring(j, j+4).equals("<!--")) {
					j = parseComment(j, false);
					if(j < 0)
						return j;
					continue;
				}
				
				NamePos result = scanName(j+2, declstartpos);
				String name = result.getName();
				j = result.getPos();
				
				if(j == -1)
					return -1;
				
				if(!"attlist".equals(name) &&
					!"attelement".equals(name) &&
					!"entity".equals(name) &&
					!"notation".equals(name)) {
					
					updatePos(declstartpos,j+2);
					error("unknown declaration " + name + "in internal subset");
				}
				
				j = parseDocType(name, j,declstartpos);

				if(j < 0)
					return j;
			} else if(c == '%') {
				//parameter entity reference
				if((j+1) == n) {
					//end of buffer, incomplete
					return -1;
				}
				
				NamePos result = scanName(j+1, declstartpos);
				j = result.getPos();
				
				if(j < 0)
					return j;
				
				if(buf.charAt(j) == ';')
					j++;
				
			} else if(c == ']') {
				j++;
				while(j < n && Character.isWhitespace(buf.charAt(j)))
						j++;
				
				if(j < n) {
					if(buf.charAt(j) == '>')
						return j;
					updatePos(declstartpos,j);
					error("unexpected char after internal subset");
				} else {
					return -1;
				}
				
			} else if(Character.isWhitespace(c)) {
				j++;
			} else {
				updatePos(declstartpos, j);
				error("unexpected char after internal subset");
			}
		}
		//end of buffer reached
		return -1;
	}
	
	private int parseDocType(String name, int i, int declstartpos) {

		if("attlist".equals(name)) {
			return parseDocTypeAttList(i, declstartpos);
		} else if("element".equals(name)) {
			return parseDocTypeElement(i, declstartpos);
		} else if("notation".equals(name)) {
			return parseDocTypeNotation(i, declstartpos);
		} else if("entity".equals(name)) {
			return parseDocTypeEntity(i, declstartpos);
		}
		
		return -1;
	}
	
	
	//scan past <!ELEMENT declarations
	private int parseDocTypeElement(int i, int declstartpos) {
		NamePos result = scanName(i, declstartpos);
		int j = result.getPos();
		
		if(j == -1)
			return -1;
		
		String buf = rawdata;
		if(buf.substring(j).contains(">"))
			return buf.indexOf('>', j) + 1;
		
		return -1;
	}

	//scan past <!ATTLIST declarations
	private int parseDocTypeAttList(int i, int declstartpos) {
		String buf = rawdata;
		
		NamePos result = scanName(i, declstartpos);
		int j = result.getPos();
		
		if(j >= buf.length())
			return -1;
		
		if(buf.charAt(j) == '>')
			return j+1;
		
		while(true) {
			result  = scanName(j, declstartpos);
			j = result.getPos();

			if(j < 0)
				return j;
		
			if(j >= buf.length())
				return -1;
			
			if(buf.charAt(j) == '(') {
				//an enumerated type; look for ')'
				
				j = buf.indexOf(')',j) + 1;
				
				if(j < 0)
					return -1;
				while(Character.isWhitespace(buf.charAt(j)))
					j++;
				
				//end of buffer; incomplete
				if(j > buf.length())
					return -1;
			} else {
				result = scanName(j, declstartpos);
				j = result.getPos();
			}
			
			if(j >= buf.length())
				return -1;
			
			char c = buf.charAt(j);
			
			if("'\"".indexOf(c) > -1) {
				Matcher matcher = _DECLSTRINGLIT_MATCH.matcher(buf.substring(j));
				if(matcher.matches())
					j = matcher.end();
				else
					return -1;
				
				if(j >= buf.length())
					return -1;
			}
			
			if(c == '#') {
				if(j == (buf.length() -1)) //end of buffer
					return -1;
				
				result = scanName(j+1,declstartpos);
				j = result.getPos();
				
				if(j < 0)
					return j;
				
				if(j >= buf.length())
					return -1;
				
				c = buf.charAt(j);
			}
			
			if(c == '>') {
				//all done
				return -1;
			}
		}
	}
	
	private int parseDocTypeNotation(int i, int declstartpos) {
		NamePos result = scanName(i, declstartpos);
		
		int j = result.getPos();
		
		if(j < 0)
			return j;
		
		String buf = rawdata;
		
		while(true) {
			if(j >= buf.length())
				return -1;
			
			char c = buf.charAt(j);
			
			if(c == '>')
				return j+1;
			
			if("'\"".indexOf(c) > -1) {
			
				Matcher matcher = _DECLSTRINGLIT_MATCH.matcher(buf.substring(j));
				if(!matcher.matches())
					return -1;
				j = matcher.end();

			} else {
				
				result = scanName(j, declstartpos);
				j = result.getPos();
				if(j < 0)
					return j;
			}
				
		}
	}

	int parseDocTypeEntity(int i, int declstartpos) {
		String buf = rawdata;
		
		int j;
		if(buf.charAt(i) == '%') {
			j = i + 1;
			while(true) {

				if(j >= buf.length())
					return -1;
				
				if(Character.isWhitespace(buf.charAt(j)))
					j++;
				else
					break;
			}
		} else {
			j = i;
		}
		
		NamePos result = scanName(j, declstartpos);
		
		j = result.getPos();

		if(j > 0)
			return j;
		
		while(true) {
			
			if(j >= buf.length())
				return -1;
			
			char c = buf.charAt(j);
			
			if("'\"".indexOf(c) > -1) {
				Matcher matcher = _DECLSTRINGLIT_MATCH.matcher(buf.substring(j));
				if(matcher.matches())
					j = matcher.end();
				else
					return -1;
			} else if(c == '>'){
				return j+1;
			} else {
				result = scanName(j, declstartpos);
				if(j < 0)
					return j;
			}
		}
	}
	
	/*
	 * since we can't return tuples in java, we'll just have to do with
	 * a StringBuffer to store the tag name.
	 */
	private NamePos scanName(int i, int declstartpos) {
		String buf = rawdata;
		int n = buf.length();
		
		if(i == n)
			return new NamePos(-1,null);

		Matcher matcher = _DECLNAME_MATCH.matcher(buf);
		
		if(matcher.find(i)) {
			String s = matcher.group();
			String found_name = s.trim();
			if((i + s.length()) == n)
				return new NamePos(-1,null);

			return new NamePos(matcher.end(), found_name);
		} else {
			updatePos(declstartpos, i);
			error("Expected name token at: " + buf.substring(declstartpos));
			return new NamePos(-1, null);
		}
	}
	
	protected abstract void unknownDecl(String data);
	protected abstract void handleDecl(String decl);
	protected abstract void handleComment(String data);
	
	private class NamePos {
		private int pos;
		private String name;

		public NamePos(int pos, String name) {
			this.pos = pos;
			this.name = name;
		}
		
		public void setPos(int pos) { this.pos = pos; }
		public int getPos() { return pos; }
		
		public void setName(String name) { this.name = name; }
		public String getName() { return name; }
	}
}
